Marginalia — Cuaderno Interactivo Marginalia Chapter 1: Aritmetic and Number Theory in C
En el Capítulo 1 se contextualiza la estrecha e ineludible relación entre la criptografía moderna y la teoría de números (el estudio de los números naturales). El autor aclara desde el principio el alcance del libro: no pretende profundizar en la matemática pura y abstracta de manera oceánica, sino enfocarse estrictamente en las herramientas necesarias para la aplicación criptográfica, reconociendo que el nivel de profundidad entre ambas disciplinas no tiene límites.
Raíces de la teoría de números
- Los Pitagóricos (Siglo VI a.C.e.): Quienes veneraban de forma casi religiosa la idea de que todo el universo físico y matemático era conmensurable a través de los números naturales. Destacando el colapso de su cosmovisión al descubrir de la existencia de los números irracionales (como \(\sqrt{2}\)), un hallazgo que intentaron suprimir censurando el conocimiento.
- Algoritmos de la Antigüedad Clásica: Presenta a Euclides (Siglo III a.C.e.) y a Eratóstenes (276–195 a.C.e.) como pilares fundamentales. Dandonos una paradoja fascinante: el "algoritmo de Euclides" y la "criba de Eratóstenes", diseñados hace más de dos milenios, son piezas de software completamente vigentes y críticas en los sistemas de cifrado actuales que aseguran el tráfico de datos en el Internet moderno.
La dualidad matemática de la Criptografía
Es el enunciado fundacional del libro. Define el terreno de juego (la teoría de números) pero establece un límite pragmático de ingeniería: aprender a usar el tesoro matemático sin perderse en la abstracción pura.
"TO BE INVOLVED WITH MODERN cryptography is to dive willy-nilly into number theory, that is, the study of the natural numbers, one of the most beautiful areas of mathematics. However, we have no intention of becoming deep-sea divers who raise sunken treasure from the mathematical ocean floor, which in any case is unnecessary for cryptographic applications."
El autor utiliza la metáfora del "océano matemático" para delimitar la frontera entre el científico de la computación / criptógrafo aplicado y el matemático teórico. La criptografía no requiere demostrar cada conjetura abierta de la teoría de números, sino explotar las propiedades computacionales de las estructuras algebraicas ya descubiertas (como los grupos cíclicos o los campos finitos). La expresión "willy-nilly" (quieras o no) enfatiza que es imposible programar criptografía asimétrica robusta (como RSA o curvas elípticas) tratando la matemática subyacente como una simple caja negra; se requiere comprender la naturaleza de los enteros.
El dogma pitagórico y la censura del conocimiento
Ilustra un patrón histórico repetitivo: el choque entre la teoría dogmática y la realidad factual, y cómo el miedo al quiebre de un paradigma conduce a la supresión de la información (un concepto íntimamente ligado hoy en día a la historia de la criptografía y los secretos gubernamentales).
"With religious zeal they took the position that all numbers should be commensurate with the natural numbers, and they found themselves on the horns of a serious dilemma when they discovered the existence of 'irrational' numbers such as \(\sqrt{2}\)… This discovery threw the world view of the Pythagoreans into disarray, to the extent that they sought to suppress knowledge of the irrational numbers, a futile form of behavior oft repeated throughout human history."
El descubrimiento de que \(\sqrt{2}\) no puede expresarse como la fracción de dos enteros rompió la filosofía mística pitagórica de que el cosmos estaba gobernada exclusivamente por proporciones armónicas enteras. En lugar de adaptar su teoría a la evidencia, recurrieron a la ocultación. Welschenbach introduce esto sutilmente para recordarnos que la verdad matemática no se puede decretar ni esconder por motivos ideológicos o políticos. En computación y criptografía, este comportamiento "fútil" se ve reflejado en la seguridad por oscuridad (intentar ocultar un algoritmo defectuoso en lugar de someterlo al escrutinio público).
La vigencia intemporal del algoritmo
Establece el puente directo entre la antigüedad clásica y la tecnología de redes del siglo XXI, justificando por qué un programador moderno de C/C++ debe estudiar textos de hace 2300 años.
"Two of the oldest number-theoretic algorithms… are closely related to the most contemporary encryption algorithms that we use every day to secure communication across the Internet. The 'Euclidean algorithm' and the 'sieve of Eratosthenes' are both quite up-to-date for our work…"
Esta cita desarma el prejuicio de que en las ciencias de la computación todo lo viejo es obsoleto. El Algoritmo de Euclides (para hallar el Máximo Común Divisor) es el motor que permite calcular inversos modulares indispensables para generar llaves en RSA. La Criba de Eratóstenes sigue siendo la base conceptual para el testeo y generación de números primos grandes. El autor argumenta implícitamente que la eficiencia en la seguridad informática contemporánea descansa sobre bloques lógicos de piedra que han resistido milenios de análisis humano.
Para entender la trascendencia de lo que Welschenbach expone en la página 3 sobre la vigencia de los algoritmos de la antigüedad, es necesario contrastar su postura pragmática con la frontera de la investigación contemporánea. El autor afirma que la teoría de números, antes vista como matemática pura e inaccesible, es hoy el escudo de la infraestructura digital. Este fenómeno de metamorfosis epistemológica es analizado a fondo por Atan (2017), quien provee el marco histórico de cómo las propiedades "ocultas" de los números naturales pasaron de ser curiosidades teóricas a convertirse en los cimientos de los criptosistemas globales.
Ahora bien, Welschenbach introduce el Algoritmo de Euclides y la Criba de Eratóstenes como herramientas completamente actuales, y la literatura científica reciente demuestra que esto no es una exageración histórica, sino un desafío de ingeniería activo:
- La evolución de Euclides y el cálculo del GCD: En sistemas embebidos y servidores de alto rendimiento, calcular el Máximo Común Divisor de números masivos de forma secuencial es un cuello de botella. Para resolver esto, Pathirana et al. (2020) demuestran cómo el principio de Euclides debe ser paralelizado en arquitecturas multinúcleo modernas utilizando OpenMP para mantener la viabilidad computacional de protocolos como RSA.
- La optimización del Inverso Modular: Welschenbach menciona que el algoritmo euclidiano es el motor detrás de la criptografía asimétrica. Al respecto, Deora (2023) propone romper el esquema clásico recursivo (top-down y bottom-up) del Algoritmo de Euclides Extendido, desarrollando un enfoque puramente lineal ("top-down") para resolver ecuaciones diofánticas bidimensionales, lo que reduce drásticamente el tiempo de cómputo al generar llaves criptográficas.
- La herencia de Eratóstenes en la generación de Primos: La criba clásica sirve para encontrar primos pequeños, pero RSA exige primos colosales de forma instantánea. El trabajo de Assa-Agyei et al. (2025) toma el problema raíz de la densidad y búsqueda de números primos en la era cuántica y propone técnicas avanzadas para acelerar su generación, conectando la teoría clásica con la eficiencia matemática que exige el cifrado moderno.
El legado histórico moderno y la fundamentación lógica de los números mediante la teoría de conjuntos.
En primer lugar, el autor enumera a los fundadores de la teoría de números moderna: Fermat, Euler, Legendre, Gauss y Kummer. Afirma que sus desarrollos teóricos abstractos constituyen la base matemática indispensable para la criptografía asimétrica contemporánea y las firmas digitales (temas que se tratarán en el Capítulo 17). Recomienda encarecidamente el libro Fermat's Last Theorem de Simon Singh para profundizar en la crónica humana y matemática de estos personajes.
En segundo lugar, Welschenbach aborda el problema de la justificación teórica de la aritmética elemental (como el hecho de que \(2 + 2 = 4\)). Para ello, introduce la construcción axiomática de los números naturales a partir del conjunto vacío (\(\emptyset\) o \(\{~\}\)), donde cada número se define rigurosamente a través del concepto de "sucesor" usando la regla \(x^+ := x \cup \{x\}\).
- El \(0\) se asocia al conjunto vacío: \(\emptyset\).
- El \(1\) es el sucesor de \(0\): \(0^+ = \{0\} = \{\emptyset\}\).
- El \(2\) es el sucesor de \(1\): \(1^+ = \{0, 1\} = \{\emptyset, \{\emptyset\}\}\).
Este proceso se extiende al infinito gracias al Axioma de Infinitud, garantizando la existencia de un conjunto sucesor mínimo denotado como \(\mathbb{N}\) (el conjunto de los números naturales). Finalmente, el autor justifica formalmente mediante una nota al pie la inclusión explícita del cero (\(0\)) dentro de \(\mathbb{N}\), argumentando que, más allá de la norma estándar DIN 5473, en las ciencias de la computación es una necesidad práctica debido a la indexación indexada en cero y al rol del \(0\) como elemento neutro aditivo.
Los cimientos modernos de la Criptografía Asimétrica
Identifica nominalmente a los gigantes matemáticos cuyo trabajo puro, siglos después, hizo posible la seguridad informática moderna, conectando directamente la teoría de números abstracta con la práctica del libro.
"Among the most important founders of modern number theory are to be counted Pierre de Fermat (1601–1665), Leonhard Euler (1707–1783), Adrien Marie Legendre (1752–1833), Carl Friedrich Gauss (1777–1855), and Ernst Eduard Kummer (1810–1893). Their work forms the basis for the modern development of this area of mathematics and in particular the interesting application areas such as cryptography, with its asymmetric procedures for encryption and the generation of digital signatures…" page 2
Welschenbach introduce estos nombres no por mera erudición, sino para establecer un mapa mental en el lector. Cada uno de estos matemáticos da nombre a herramientas críticas que se codificarán en este libro: el Pequeño Teorema de Fermat (esencial para los test de primalidad del Capítulo 10), la función \(\phi\) de Euler (base de la función del indicador de RSA), los símbolos de Legendre y Jacobi (Capítulo 10, indispensables para las raíces cuadradas modulares), y las estructuras algebraicas gaussianas. El autor argumenta que la infraestructura de clave pública (asímetrica) y el no-repudio (firmas digitales) no habrían existido sin la obsesión de estos hombres por las propiedades de los enteros.
La construcción de la realidad desde la nada (Set Theory)
Captura el núcleo de la construcción de von Neumann para los números naturales, demostrando cómo las matemáticas lógicas pueden erigir toda la estructura aritmética a partir de un concepto vacío.
"For example, set theory allows us to derive the existence and arithmetic of the natural numbers from (almost) nothing. This “almost nothing” is the empty (or null) set \(\emptyset := \{ \}\), that is, the set that has no elements. If we consider the empty set to correspond to the number 0, then we are able to construct additional sets as follows. The successor \(0^+\) of 0 is associated with the set \(0^+ := \{ 0 \} = \{ \emptyset \}\)…"
Esta cita explica el formalismo subyacente a la programación. En computación, los tipos de datos deben ser inicializados o construidos desde estructuras primitivas. Al definir el número entero \(x\) como un conjunto que contiene a todos sus predecesores (\(x = \{0, 1, \dots, x-1\}\)), la matemática formal modela los números no como entidades místicas, sino como colecciones ordenadas de información anidada. Esto se traduce de forma limpia en estructuras de datos de bajo nivel; un número entero en memoria no es más que una acumulación secuencial de estados que inicia desde un bit en cero.
El pragmatismo informático del número cero
Representa un puente directo entre el debate matemático clásico (si el cero es o no un número natural) y la convención imperativa del diseño de software y la arquitectura de hardware.
"From the point of view of computer science, however, it is practical to begin counting at zero instead of 1, which is indicative of the important role played by zero as the neutral element for addition (additive identity)."
El autor aborda la clásica división filológica y matemática sobre el cero. Mientras que muchas escuelas matemáticas tradicionales excluyen al cero de los números contables (\(\mathbb{N}^*\)), Welschenbach defiende la postura de las ciencias de la computación. En la arquitectura de computadoras, el cero es el desplazamiento (offset) inicial en los bloques de memoria (el primer elemento de un arreglo está a una distancia de \(0\) unidades del puntero base). Además, computacionalmente, el cero como identidad aditiva (\(a + 0 = a\)) es el estado por defecto de los registros del procesador (como el acumulador) antes de iniciar cualquier ciclo o sumatoria cíclica.
En el siguiente apartado se buscara entender el comportamiento de los números naturales utilizando los Axiomas de Peano y demuestra cómo, a partir de ellos, se autoconstruyen las operaciones de suma, multiplicación y exponenciación de forma recursiva.
El autor presenta tres axiomas fundamentales de Peano:
- Inyectividad de los sucesores: Si dos números son distintos, sus sucesores también lo son (\(n \neq m \implies n^+ \neq m^+\)). No hay colisiones en la recta numérica.
- El cero como origen: El cero es el único número que no tiene un predecesor en \(\mathbb{N}\). Todos los demás números sí provienen de alguien.
- Principio de Inducción Completa: Si una propiedad se cumple para el \(0\), y suponiendo que se cumple para un número \(n\) logramos demostrar que se cumple para su sucesor \(n^+\), entonces la propiedad es válida para absolutamente todos los números naturales.
A partir del Axioma de Inducción, Welschenbach muestra que las operaciones aritméticas básicas no son mágicas, sino funciones recursivas:
- La Suma (\(n + x\)): Se define mediante una función de acumulación \(s_n(x)\). Sumar es simplemente aplicar el operador sucesor (\(^+\)) de forma iterativa.
- La Multiplicación (\(n \cdot x\)): Se define mediante una función \(p_n(x)\) que acumula sumas repetidas. Multiplicar es sumar el número \(n\) de forma recursiva.
- La Exponenciación: Se obtiene de manera análoga, acumulando multiplicaciones repetidas.
Propiedades esenciales como la asociatividad, conmutatividad y distributividad se derivan matemáticamente de estos axiomas, y advierte que comprender esta estructura es vital porque serán las reglas que usaremos para verificar que nuestras funciones en código C++ (la librería FLINT) operen de manera correcta y sin errores lógicos.
Los tres pilares de la recta numérica (Axiomas de Peano)
Define formalmente el esqueleto lógico de los números que el procesador va a computar. Sin estas tres reglas, la aritmética en memoria no tendría consistencia interna.
"The natural numbers can be characterized by means of the axioms of Giuseppe Peano (1858–1932)… (I) The successors of two unequal natural numbers are unequal: From \(n \neq m\) it follows that \(n^+ \neq m^+\). (II) Every natural number, with the exception of 0, has a predecessor. (III) The principle of complete induction: If \(S \subset \mathbb{N}\), \(0 \in S\), and \(n \in S\) always imply \(n^+ \in S\), then \(S = \mathbb{N}\)." page 5
Para un programador, los axiomas de Peano se traducen directamente en el comportamiento de los tipos de datos enteros (`int` o la clase `LINT` del libro):
- El Axioma I garantiza la unicidad. Si incrementas dos variables con valores diferentes, sus resultados seguirán siendo diferentes. Evita que dos operaciones distintas apunten a la misma dirección de memoria o valor.
- El Axioma II establece el límite inferior. El cero es el anclaje del sistema, el caso base. En programación, impide que un bucle iterativo hacia atrás decaiga al infinito de forma indefinida dentro de los enteros naturales.
- El Axioma III (Inducción) es el fundamento de los bucles (`while`, `for`) y la recursividad. Si puedes inicializar un estado en \(0\) (caso base) y definir un paso de incremento seguro (estado \(n \to n^+\)), el bucle computará correctamente cualquier entrada sin importar su tamaño.
La definición formal de la Suma mediante código matemático
Muestra la definición matemática exacta de la suma. Es la base conceptual de cómo un circuito o una función de software incrementa un valor de manera primitiva.
"The fundamental operations of addition and multiplication can be defined recursively as follows. We begin with addition: For every natural number \(n \in \mathbb{N}\) there exists a function \(s_n\) from \(\mathbb{N}\) to \(\mathbb{N}\) such that (i) \(s_n(0) = n\) (ii) \(s_n(x^+) = (s_n(x))^+\) for all natural numbers \(x \in \mathbb{N}\)."
Aquí el autor desmenuza la operación \(n + x\) convirtiéndola en un algoritmo puro. Vamos a traducirlo a cómo piensa un programador:
- El caso (i) \(s_n(0) = n\) significa: "Si a \(n\) le sumas \(0\), el resultado es simplemente \(n\)". Es la condición de parada o el caso base de la función.
- El caso (ii) \(s_n(x^+) = (s_n(x))^+\) significa: "Sumar el sucesor de \(x\) es equivalente a sumarle \(x\) a \(n\) y luego sacarle el sucesor al resultado".
Ejemplo: Si queremos sumar \(5 + 2\), sabemos que \(2\) es el sucesor de \(1\) (\(1^+\)). Entonces: \[5 + 2 = 5 + 1^+ = (5 + 1)^+\] A su vez, \(1\) es el sucesor de \(0\) (\(0^+\)): \[(5 + 0^+)^+ = ((5 + 0)^+)^+\] Aplicando el caso base, \(5 + 0 = 5\), por lo que nos queda el sucesor del sucesor de \(5\): \[((5)^+)^+ = (6)^+ = 7\] En software, esto demuestra que la suma no es más que un contador que ejecuta el operador de incremento (`++`) exactamente \(x\) veces.
La construcción de la Multiplicación sobre la Suma
Establece la jerarquía algorítmica: la multiplicación no existe por sí misma, es una abstracción de nivel superior construida sobre bucles de sumas.
"For multiplication one proceeds analogously: For every natural number \(n \in \mathbb{N}\) there exists a function \(p_n\) from \(\mathbb{N}\) to \(\mathbb{N}\) such that (i) \(p_n(0) = 0\) (ii) \(p_n(x^+) = p_n(x) + n\) for all natural numbers \(x \in \mathbb{N}\)."
El autor replica la lógica recursiva para la operación \(n \cdot x\):
- El caso (i) \(p_n(0) = 0\) es el límite: todo número multiplicado por cero es cero.
- El caso (ii) \(p_n(x^+) = p_n(x) + n\) define la multiplicación como una acumulación. Significa: "Multiplicar \(n\) por el sucesor de \(x\) es igual a lo que ya tenías acumulado (\(n \cdot x\)) más otra vez \(n\)".
En ciencias de la computación, esto justifica por qué la multiplicación en la Unidad Aritmético Lógica (ALU) del procesador consume más ciclos de reloj que una suma ordinaria: el hardware subyacente tiene que realizar ciclos iterativos de adición combinados con desplazamientos de bits (shifts) para poder obtener el producto.
Welschenbach advierte que la exponenciación matemática se construye de forma análoga e inductiva a partir de la multiplicación iterada. En el software del mundo real, sin embargo, implementar la exponenciación siguiendo únicamente la definición inductiva elemental de Peano generaría un costo computacional inasumible (\(O(2^k)\)), inutilizando cualquier criptosistema. Para solventar este abismo entre la teoría pura y la viabilidad de la ingeniería, la literatura científica actual se enfoca en optimizaciones críticas de bajo nivel. Una de las más importantes es la sustitución de la reducción modular clásica por el espacio matemático de Montgomery, una técnica analizada por el equipo de Cybernetics and Systems Analysis (2024), donde demuestran cómo la multiplicación de Montgomery elimina las divisiones costosas en el procesador para acelerar drásticamente la exponenciación de números de múltiples bits (la base de lo que el libro enseñará en el Capítulo 6).
No obstante, la optimización algorítmica introduce un peligro invisible en la ejecución física de nuestro código en C/C++. Cuando el procesador ejecuta los bucles iterativos condicionales de la exponenciación (como los algoritmos de "Square-and-Multiply"), el consumo de energía eléctrica del chip varía de acuerdo a si está procesando un bit '0' o un bit '1'. Esta fuga de información física permite los llamados ataques de canal lateral. Una muestra alarmante del estado del arte en este ámbito es el trabajo publicado en IACR (2025), el cual demuestra que es posible romper implementaciones criptográficas mediante un análisis simple de energía (SPA) usando una única traza de corriente y sin entrenamiento previo, evidenciando que el programador de software criptográfico debe diseñar pensando en la resistencia física del hardware.
Frente a esta vulnerabilidad, la ingeniería moderna recurre al "enmascaramiento" (masking) aritmético para alterar los datos aleatoriamente en memoria sin cambiar el resultado matemático final. Sin embargo, modificar las funciones de la librería para ocultar el consumo energético puede corromper la lógica exacta del programa. Por esta razón, investigaciones como la de LNCS (2023) desarrollan sistemas de verificación automatizada de corrección matemática para asegurar que, al proteger el software contra ataques físicos, no se alteren las leyes y axiomas aritméticos primitivos que Peano e inductores clásicos definieron en la teoría pura.
La estructuración axiomática de la aritmética elemental definiendo la exponenciación (\(n^x\)) como una función recursiva \(e_n(x)\) bajo dos condiciones:
- Caso base: Cualquier número elevado a la potencia cero es uno (\(e_n(0) = 1\)).
- Paso inductivo: Elevar \(n\) al sucesor de \(x\) equivale a multiplicar el acumulado anterior por la base \(n\) (\(e_n(x^+) = e_n(x) \cdot n\)).
Mediante inducción completa se demuestran las leyes de los exponentes (como \(n^x n^y = n^{x+y}\) o \((n^x)^y = n^{xy}\)), las cuales serán la base del desarrollo del Capítulo 6. Adicionalmente, introduce la relación de orden (\(<\)) en \(\mathbb{N}\) para permitir la comparación de elementos, dejando de lado la rigurosidad abstracta de conjuntos para asumir sus propiedades intuitivas cotidianas.
Este viaje axiomático mostradi en (páginas 3 a 6) debe verse como un proceso de "división celular matemática": partiendo únicamente del conjunto vacío (\(\emptyset\)) como bloque constructor fundamental, la lógica pura dio vida a los números, sus operaciones y sus reglas de interacción.
La definición recursiva de la potencia (\(n^x\))
Completa la trilogía de operaciones primitivas. Al igual que la suma y la multiplicación, la potencia se define formalmente como un proceso iterativo de multiplicación acumulada.
"For every natural number \(n \in \mathbb{N}\) there exists a function \(e_n\) from \(\mathbb{N}\) to \(\mathbb{N}\) such that (i) \(e_n(0) = 1\), (ii) \(e_n(x^+) = e_n(x) \cdot n\) for every natural number \(x \in \mathbb{N}\)."
Esta definición matemática es el plano de diseño de la función de exponenciación que se usará en criptografía asimétrica.
- El caso (i) establece que \(n^0 = 1\). Computacionalmente, este es el valor de inicialización de nuestro registro acumulador.
- El caso (ii) indica que para calcular \(n^{x+1}\), tomamos el resultado de \(n^x\) y lo multiplicamos por una base \(n\) adicional.
En C/C++, esto se traduce directamente en un bucle donde multiplicamos la base por sí misma de manera repetida. La gran diferencia en criptografía (Capítulo 6) es que nunca calculamos \(n^x\) a secas porque el resultado tendría millones de dígitos y desbordaría la memoria; en su lugar, calculamos la exponenciación modular (\(n^x \pmod m\)), aplicando el residuo en cada paso del bucle para mantener el número acotado dentro de los límites del hardware.
La metáfora de la división celular matemática
Sintetiza la filosofía analítica del autor. Explica el propósito de haberle mostrado al lector la teoría de conjuntos antes de empezar a escribir código.
"…it is nonetheless of interest to us to have at least once taken a glance at a process of 'mathematical cell division,' a process that produces not only the natural numbers, but also the arithmetic operations and rules with which we shall be deeply involved from here on." page 6
Welschenbach utiliza el término "división celular" de manera sumamente acertada. En la biología, una sola célula se divide y especializa para formar sistemas orgánicos complejos. En la aritmética formal ocurre lo mismo: el conjunto vacío \(\emptyset\) se clona y anida a sí mismo a través del operador sucesor para dar origen al \(1\), al \(2\) y al infinito. De esa misma "división" nacen las reglas de la suma, y sobre la suma nace la multiplicación. El argumento del autor es que, en programación de sistemas, para construir una librería robusta de criptografía (como FLINT/C), el ingeniero debe adoptar este mismo enfoque modular: construir funciones primitivas ultra-simples y perfectas, y luego usarlas como bloques para erigir algoritmos masivos (como RSA).
Presentación formal de FLINT/C
Marca el punto de inflexión del libro. Termina la introducción teórica abstracta e inicia el manual de implementación de software.
"The software described in this book constitutes in its entirety a package, a so-called function library… This library has been given the name FLINT/C, which is an acronym for 'functions for large integers in number theory and cryptography.'"
En la computación estándar de C/C++, los tipos nativos como `uint64t` tienen un límite estricto de tamaño (\(2^{64}-1\)). La criptografía moderna requiere procesar números de 2048, 4096 o más bits. FLINT/C resuelve esto rompiendo la dependencia del hardware y creando estructuras de datos orientadas a objetos (o arreglos de dígitos) controladas por software. El autor introduce el nombre de la librería para recordarle al programador que cada línea de código que se escribirá en los siguientes capítulos tiene un objetivo doble: cumplir con el rigor de la teoría de números y garantizar el rendimiento óptimo para la seguridad informática.
Para entender la distribución de la librería FLINT/C, el autor desglosa las funciones en tres categorías críticas: lógica algorítmica en C, aceleración por hardware en ensamblador y vectores de prueba.
| Directorio Base | Módulo / Archivo | Propósito e Implementación en la Librería |
|---|---|---|
| /flint/src | flint.h | Interfaz y cabecera principal de funciones. |
| /flint/src | flint.c | Operaciones aritméticas y teoría de números en C puro. |
| /flint/src | kmul.{h,c} | Multiplicación y elevación al cuadrado mediante Karatsuba. |
| /flint/src | ripemd.{h,c} | Implementación del algoritmo criptográfico hash RIPEMD-160. |
| /flint/src | sha{1,256}.{h,c} | Implementación de funciones hash seguras SHA-1 y SHA-256. |
| /flint/src | entropy.c | Captura de entropía como semilla de aleatoriedad. |
| /flint/src | random.{h,c} | Generador de números pseudoaleatorios (PRNG). |
| /flint/src | aes.{h,c} | Implementación del Advanced Encryption Standard (Rijndael). |
| /flint/src/asm | mult.{s,asm} | Reemplazo optimizado en ensamblador de la función mult(). |
| /flint/src/asm | umul.{s,asm} | Reemplazo optimizado en ensamblador de la función umul(). |
| /flint/src/asm | sqr.{s,asm} | Elevación al cuadrado optimizada para bajo nivel. |
| /flint/src/asm | div.{s,asm} | División optimizada, reemplaza a divl(). |
| /flint/test | testxxx.c[pp] | Programas de testeo y validación unitaria en C/C++. |
| /flint/testvals | xxx.txt | Vectores de prueba estáticos para validación de AES. |
—
Al analizar el mapa de archivos, se hace evidente la estrategia del autor para balancear portabilidad y alto rendimiento, dos conceptos clave en el desarrollo de sistemas criptográficos modernos:
- La Capa de Abstracción en C (Portabilidad): Los archivos dentro de
/flint/srcproveen una base genérica escrita en C estándar. Esto garantiza que la librería pueda compilarse y ejecutarse prácticamente en cualquier arquitectura o sistema operativo (desde una PC de escritorio hasta sistemas embebidos de bajo consumo), sirviendo como el motor lógico base del libro. - La Capa de Aceleración en Ensamblador (Rendimiento): En la criptografía de clave pública, operaciones como la multiplicación (
mult,umul) y la división (div) sobre números gigantescos se ejecutan millones de veces por segundo. Dejar esto en manos del compilador de C no siempre genera el código binario más eficiente. Por ello, el autor introduce el directorio/flint/src/asmpara reemplazar esas funciones críticas con instrucciones ensamblador específicas de la arquitectura x86. Esto permite exprimir directamente los registros de la CPU y los acarreos de hardware, logrando la velocidad que el software de producción exige. - Optimización con Karatsuba (
kmul): La inclusión de un archivo exclusivo para la multiplicación de Karatsuba demuestra que el libro no se limita a la aritmética de la escuela primaria (\(O(n^2)\)). Para números con un gran volumen de bits, este algoritmo divide los números en partes más pequeñas reduciendo la complejidad del proceso de multiplicación a aproximadamente \(O(n^{1.585})\), una optimización matemática crucial que estudiarás más adelante. - La Rigurosidad del Testeo (
/flint/test): Una librería criptográfica no solo debe ser rápida, tiene que ser matemáticamente exacta. Un solo bit erróneo rompería por completo el proceso de descifrado. Los vectores de prueba (xxx.txt) para AES actúan como "llaves maestras" precalculadas internacionalmente (NIST) para certificar que tu implementación genera exactamente los mismos resultados lógicos que los estándares de seguridad mundiales.
Estructura de Enlazado y Clases de Alto Nivel (FLINT/C y C++)
Para dar soporte multiplataforma y facilitar el uso del software en aplicaciones reales, el autor organiza las librerías binarias resultantes y los módulos de abstracción criptográfica (RSA).
| Directorio Base | Archivo / Componente | Propósito del Módulo e Interfaz |
|---|---|---|
| /flint/lib | flinta.lib | Librería con funciones ASM en formato OMF (Object Module Format). |
| /flint/lib | flintavc.lib | Librería con funciones ASM en formato COFF (Common Object File Format). |
| /flint/lib | flinta.a | Archivo estático para sistemas operativos emx/gcc bajo OS/2. |
| /flint/lib | libflint.a | Archivo estático nativo para sistemas operativos LINUX. |
| /flint/lib | flint.dll | Librería de Enlace Dinámico (DLL) para MS VC++. |
| /flint/lib | flint.lib | Librería de importación y enlazado para flint.dll. |
| /flint/rsa | rsakey.h | Archivo de cabecera para las clases estructurales de RSA. |
| /flint/rsa | rsakey.cpp | Implementación de las clases orientadas a objetos RSAkey y RSApub. |
| /flint/rsa | rsademo.cpp | Programa de demostración del criptosistema RSA de extremo a extremo. |
—
El Flujo de Compilación y Gestión del Stack
Empecemos con un comando de compilación monolítico clásico para enlazar el motor matemático en C con la interfaz de usuario orientada a objetos en C++:
gcc -O2 -o rsademo rsademo.cpp rsakey.cpp flintpp.cpp randompp.cpp flint.c aes.c ripemd.c sha256.c entropy.c random.c -lstdc++
- Portabilidad de Cabeceras (
FLINTPP_ANSI): El macroFLINTPP_ANSIse utilizaba históricamente para alternar entre las cabeceras estándar modernas de C++ (#include <iostream>) y las pre-estándar de sistemas antiguos (#include <iostream.h>).
Un peligro latente en sistemas embebidos o antiguos: las funciones de exponenciación modular (esenciales para RSA) imponen una demanda masiva sobre la pila (Stack).
- Nota de Contexto Moderno (Nota al pie 3): El propio autor reconoce que en sistemas Unix/Linux modernos con memoria virtual, el tamaño por defecto del Stack suele ser suficiente (típicamente 8 MB, verificable en tu terminal con
ulimit -s). - Mitigación Arquitectónica: El libro resuelve este problema estructural más adelante mediante dos estrategias:
- Uso de exponenciación con asignación dinámica de memoria (en el
Heap). - Implementación de Registros Dinámicos (analizados a fondo en el Capítulo 9).
- Uso de exponenciación con asignación dinámica de memoria (en el
Para permitir que el enlazador (Linker) identifique correctamente si una función está escrita en C puro, en Ensamblador nativo, o si es una constante exportada, la librería utiliza prefijos de macros de compatibilidad:
__FLINT_API: Calificador para funciones nativas escritas en C.__FLINT_API_A: Calificador exclusivo para optimizaciones en ensamblador (ASM).__FLINT_API_DATA: Calificador para tablas de constantes globales (como la lista de números primos pequeñossmallprimes[]).
En sistemas Unix/Linux estándar, estas macros se compilan como comentarios vacíos (/**/), pero son críticas si el código se porta a compiladores comerciales estrictos con convenciones de llamada específicas (__cdecl, __stdcall).
Gestión de Estado, Inicialización y Seguridad Crítica
Las macros __FLINT_API_A y __FLINT_API se expanden a atributos específicos del compilador como __cdecl o __declspec(dllimport) cuando se compila en entornos no corporativos o sistemas Windows.
- Impacto en Gentoo / Linux: En arquitecturas modernas x8664 bajo Linux, la convención de llamadas estándar (System V AMD64 ABI) pasa los primeros argumentos a través de registros del procesador (
RDI,RSI,RDX, etc.) de forma nativa. Por lo tanto, bajo GCC en Linux estas macros simplemente se disuelven en código vacío sin penalización de rendimiento, manteniendo la portabilidad intacta.
El Ciclo de Vida del Motor: FLINTInit_l() y FLINTExit_l()
La librería no puede operar de forma aislada; requiere una rutina de inicialización global de estado:
FLINTInit_l(): Se encarga de dos tareas críticas al arrancar la librería:- Generar la semilla inicial para el Generador de Números Pseudoaleatorios (PRNG) tomando valores de 32 bits del reloj de tiempo real del sistema.
- Asignar las estructuras de los registros dinámicos que se usarán a lo largo del procesamiento del software.
FLINTExit_l(): Actúa como el recolector de basura de bajo nivel, liberando y desasignando por completo los recursos del sistema antes de cerrar la sesión.- > Nota de Seguridad Vial (Nota al pie 4): El autor advierte explícitamente que usar el reloj del sistema como semilla criptográfica es sumamente peligroso para software de producción en seguridad crítica, requiriendo su reemplazo por entropía real del sistema operativo (como
/dev/urandomen Linux).
En software criptográfico no basta con dejar de usar una variable; si una clave privada permanece latente en la pila o el montón (Heap), un atacante con acceso a un volcado de memoria o mediante un exploit de lectura residual podría extraer los secretos.
- Modo Seguro (
Security Mode): Por defecto, al terminar el ámbito de un objeto (CLINToLINT), la librería invoca destructores automáticos o la macroPURGEVARS_L()para sobrescribir con ceros absolutos (0x00) cada bit de memoria utilizado antes de liberarlo. - Macro
FLINT_UNSECURE: Si se define esta bandera durante la compilación, se desactiva esta limpieza forzada para ahorrar ciclos de reloj a cambio de dejar el sistema expuesto frente a vulnerabilidades de recolección de memoria residual.
Para garantizar la trazabilidad de auditorías en Gentoo, la función verstr_l() devuelve una cadena de caracteres que expone la configuración exacta con la que fue construido el binario:
"X.x": Versión del software."a": Indica si las optimizaciones en Ensamblador nativo están activas."s": Confirma si el Modo Seguro de purga de memoria está encendido.
Laboratorio Práctico 1: El Límite Físico de Peano (Overflow y Comportamiento Indefinido)
Contexto del Experimento
A diferencia de los axiomas puros de Peano analizados en las páginas 5 y 6 (donde la recta numérica se extiende infinitamente sin colisiones mediante el operador sucesor \(x^+\)), el hardware real bajo una arquitectura x8664 opera con registros limitados. En este laboratorio se audita cómo reacciona el compilador GCC ante el desbordamiento de enteros con signo y sin signo, y cómo impactan las flags de optimización en la seguridad del binario.
Código de Pruebas
El archivo fuente se encuentra almacenado en scripts/01-overflowtest.c. La lógica del programa contrasta el comportamiento cíclico por hardware (unsigned) contra el Comportamiento Indefinido (UB) dictado por el estándar ISO C (signed):
#include <stdio.h>
#include <limits.h>
int main(void) {
unsigned int u_max = UINT_MAX;
unsigned int u_successor = u_max + 1;
printf("[UNSIGNED] Máximo valor: %u\n", u_max);
printf("[UNSIGNED] Sucesor (u_max + 1): %u\n\n", u_successor);
int s_max = INT_MAX;
int s_successor = s_max + 1;
printf("[SIGNED] Máximo valor: %d\n", s_max);
printf("[SIGNED] Sucesor (s_max + 1): %d\n", s_successor);
return 0;
}
A partir de la lectura de las páginas del manual de GCC (man gcc), se documenta el impacto de las directivas de generación de código utilizadas en las pruebas:
1. Gestión de Aritmética Dinámica (-fstrict-overflow, -fwrapv, -ftrapv)
- -fstrict-overflow (Por defecto en -O2 y -O3): Permite al compilador asumir rigurosamente que el desbordamiento con signo nunca va a ocurrir. Si el optimizador detecta lógica basada en un desbordamiento condicional, puede eliminarla silenciosamente para ganar velocidad, lo cual es altamente peligroso en validaciones criptográficas.
- -fwrapv: Fuerza a GCC a tratar los enteros con signo bajo la regla cíclica de Complemento a dos (wrap-around), imitando el comportamiento del
unsigned int(dondeINT_MAX + 1se convierte de forma segura y predecible enINT_MIN). Desactiva optimizaciones que asumen flujos lineales. - -ftrapv: Inyecta código de control en el ensamblador generado. Al momento exacto en que ocurre un desbordamiento con signo, el programa aborta inmediatamente enviando una señal
SIGABRT, evitando que el software procese datos corruptos.
2. Reutilización del Espacio en la Pila (-fstack-reuse)
- -fstack-reuse=all (Por defecto en optimizaciones altas): Permite a GCC reciclar las direcciones de memoria de la pila en cuanto una variable sale de su ámbito (
scopeo bloque entre llaves). - > Nota de Seguridad Criptográfica: Si un puntero residual queda apuntando a una variable local fuera de su bloque, la optimización agresiva de la pila hará que lea datos de una nueva variable, pudiendo provocar fugas catastróficas de información o corrupción de punteros si se manipulan claves criptográficas temporales.
3. Diagnóstico e Instrumentación Médica (-fsanitize=undefined)
Activa el subsistema UBSan (Undefined Behavior Sanitizer). Modifica el binario añadiendo instrumentación de monitoreo en tiempo de ejecución.
Resultados Empíricos del Laboratorio
Al compilar y ejecutar bajo diferentes ambientes en Gentoo, se obtuvieron las siguientes respuestas en la terminal:
- Compilación Estándar con -O2 (
overflow_test_02): El hardware forza el complemento a dos. El entero con signo pasa a-2147483648de forma silenciosa. - Uso de Sanitizers (
overflow_sanitized): Al ejecutar, intercepta el fallo lógico antes de imprimir el resultado e imprime:
01-overflow_test.c:12:7: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
Esto demuestra empíricamente la ruptura del modelo axiomático de Peano cuando se confía ciegamente en tipos nativos limitados por la arquitectura del chip.
Laboratorio Práctico 2: El Algoritmo del Sucesor Seguro (Control de Carry Flags)
Dado que la CPU no posee registros infinitos, para construir la librería de enteros gigantes (FLINT/C) se requiere un método para enlazar múltiples celdas de memoria. Este experimento implementa la captura matemática del bit de acarreo (Carry) antes de que se pierda por el comportamiento cíclico del hardware, utilizando las funciones intrínsecas optimizadas del compilador GCC.
Código de Pruebas (Sucesor Seguro)
El archivo fuente se encuentra en scripts/02-securesuccessor.c. Se utiliza __builtin_add_overflow para extraer directamente el estado del registro de flags del procesador:
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
typedef struct {
unsigned int value;
unsigned int carry;
} CryptoDigit;
CryptoDigit secure_successor(unsigned int current_value) {
CryptoDigit result;
result.carry = 0;
if (__builtin_add_overflow(current_value, 1, &result.value)) {
result.carry = 1;
}
return result;
}
int main(void) {
unsigned int num_limite = UINT_MAX;
CryptoDigit res_limite = secure_successor(num_limite);
printf("[LIMITE] Sucesor de %u es: %u (Acarreo/Carry: %u)\n",
num_limite, res_limite.value, res_limite.carry);
return 0;
}
Análisis del Comportamiento de Compilación
Al compilar con -O3 -march=native, GCC elimina la sobrecarga de funciones tradicionales y mapea el bloque condicional if directamente a instrucciones condicionales de bajo nivel de la arquitectura x8664 (como add seguido de un chequeo del flag de acarreo mediante instrucciones tipo setc o cmovc).
Esto demuestra que la abstracción formal de los axiomas de Peano y sus sucesores puede ser emulada de forma ultra-eficiente en software criptográfico: dividiendo un número masivo en un arreglo de celdas y propagando secuencialmente el parámetro .carry hacia los dígitos adyacentes de orden superior.
Laboratorio Práctico 3: Sumador de Precisión Múltiple Primitivo
Este laboratorio aterriza de forma práctica la definición de la suma recursiva (\(n + x\)) planteada en la página 5 de Welschenbach. Como el hardware físico está acotado por registros estáticos (32 o 64 bits), la solución de ingeniería para emular la recta numérica infinita de Peano consiste en encadenar bloques de memoria en un arreglo.
La suma multiprecisión imita el algoritmo clásico de lápiz y papel: se suman las celdas menos significativas primero, se extrae el bit de desbordamiento, y este se propaga secuencialmente hacia la izquierda como un parámetro de acarreo (Carry).
Código de Pruebas (Suma Iterativa en Bloques de 128 bits)
El archivo fuente se encuentra disponible y enlazado en scripts/03-multiprecisionadd.c. El algoritmo implementa un bucle iterativo donde cada celda utiliza dos etapas de detección de desbordamiento intrínseco de GCC para garantizar que ningún bit de acarreo se pierda en el tránsito:
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <limits.h>
#define SIZE 4
typedef struct {
unsigned int blocks[SIZE];
} BigInt;
void print_bigint(const char *label, const BigInt *num) {
printf("%s: 0x", label);
for (int i = SIZE - 1; i >= 0; i--) {
printf("%08X ", num->blocks[i]);
}
printf("\n");
}
BigInt multiprecision_add(const BigInt *a, BigInt *b) {
BigInt result;
memset(&result, 0, sizeof(BigInt));
unsigned int carry = 0;
for (int i = 0; i < SIZE; i++) {
unsigned int temp_sum;
unsigned int carry1 = 0;
unsigned int carry2 = 0;
if (__builtin_add_overflow(a->blocks[i], b->block[i], &temp_sum)) {
carry1 = 1;
}
if (__bultin_add_overflow(temp_sum, carry, &result.blocks[i])) {
carry2 = 1;
}
carry = carry1 + carry2;
}
if (carry>0) {
printf("[ALERTA] Desbordamiento total de BigInt, excedimos los 128 bits.\n");
}
return result;
}
int main(void) {
BigInt num1, num2, resultado;
memset(&num1, 0, sizeof[BigInt]);
memset(&num2, 0, sizeof[BigInt]);
num1.blocks[0] = UINT_MAX;
num1.blocks[1] = 0x00000000;
num1.blocks[2] = 0xFFFFFFFF;
num2.blocks[0] = 0x00000001;
num2.blocks[1] = 0x00000000;
num2.blocks[2] = 0x00000001;
printf("Suma multiprecision elemental de 128 bits");
printf("Numero A", &num1);
printf("Numero B", &num2);
resultado = multiprecision_add(&num1, &num2);
print_bigint("Resultado", &resultado);
return 0;
}
Al compilar este código en tu entorno Gentoo utilizando las banderas (USE flags):
gcc -O3 -funroll-loops -march=native 03-multiprecision_add.c -o multiprecision_add_O3
Se ejecutan dos optimizaciones críticas a nivel de bajo nivel:
- -funroll-loops (Desenrrollado de bucle): Como el tamaño de nuestro arreglo es una constante fija (
SIZE = 4), GCC elimina por completo la variable del contadori, las comparaciones y las instrucciones de salto (branches) del ensamblador. En su lugar, clona e hilvana secuencialmente el cuerpo del ciclo 4 veces. Esto maximiza el flujo en el pipeline de ejecución de la CPU. - Traducción a Instrucciones de Acarreo Nativo: Gracias al uso de
__builtin_add_overflow, GCC mapea el algoritmo directamente a instrucciones x8664 de adición con acarreo integrado (comoADC/ Add with Carry), permitiendo que el hardware procese enteros masivos casi a la misma velocidad que un entero nativo de la máquina.
Navegación
- Siguiente capítulo: Capítulo 2: Formatos de Representación Numérica (CLINT)